package com.zenika.zenbinding.rebind;

import java.io.PrintWriter;
import java.util.List;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

/**
 * 
 * @author Zenika
 */
public class WrapperCreator {

	private final static String BEAN_UTIL = "com.zenika.zenbinding.client.util.BeanUtil";
	private final static String WRAPPER_INTERFACE = "com.zenika.zenbinding.client.util.Wrapper";
	private final static String WRAPPER_SUFFIX = "Wrapper";
	private final static String CONTENT_INTERFACE = "com.zenika.zenbinding.client.model.BusinessObject";
	private final static String EXCEPTION = "com.zenika.zenbinding.client.exception.DataException";

	private TreeLogger logger;
	private GeneratorContext context;
	private TypeOracle typeOracle;
	private String typeName;

	public WrapperCreator(TreeLogger logger, GeneratorContext context,
			String typeName) {
		this.logger = logger;
		this.context = context;
		this.typeOracle = context.getTypeOracle();
		this.typeName = typeName;
	}

	/**
	 * Crée le code du wrapper correspondant à l'objet demandé. Puis inscrit la
	 * classe wrapper dans le treeLogger.
	 * 
	 * @return nom complet du wrapper
	 */
	public String createWrapper() {
		try {
			JClassType classType = this.typeOracle.getType(this.typeName);
			SourceWriter source = this.getSourceWriter(classType);
			// Si le wrapper existe déjà, getSourceWriter renvoie null
			// Il n'est donc pas nécessaire de créer cette classe
			if (source == null) {
				return classType.getParameterizedQualifiedSourceName()
						+ WRAPPER_SUFFIX;
			} else {
				JMethod[] methods = classType.getOverridableMethods();
				this.insertWrapperBase(source, methods, classType);
				this.insertClone(source, methods, classType);
				this.insertIsNullProperty(source, methods);
				this.insertAccessors(source, methods);
				this.insertGetProperty(source, methods);
				this.insertSetProperty(source, methods);
				source.commit(this.logger);
				return classType.getParameterizedQualifiedSourceName()
						+ WRAPPER_SUFFIX;
			}
		} catch (NotFoundException e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * SourceWriter instantiation. Return null if the resource already exist.
	 * 
	 * @return sourceWriter
	 */
	public SourceWriter getSourceWriter(JClassType classType) {
		String packageName = classType.getPackage().getName();
		String simpleName = classType.getSimpleSourceName() + WRAPPER_SUFFIX;
		ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(
				packageName, simpleName);
		composer.setSuperclass(classType.getName());
		composer.addImplementedInterface(WRAPPER_SUFFIX);
		composer.addImport(CONTENT_INTERFACE);
		composer.addImport(EXCEPTION);
		composer.addImport(WRAPPER_INTERFACE);
		composer.addImport(BEAN_UTIL);
		composer.addImport("java.util.ArrayList");
		composer.addImport("java.util.List");
		PrintWriter printWriter = this.context.tryCreate(this.logger,
				packageName, simpleName);
		if (printWriter == null) {
			return null;
		} else {
			SourceWriter sw = composer.createSourceWriter(this.context,
					printWriter);
			return sw;
		}
	}

	/**
	 * Write getters and setters of the wrapped bean's properties in the
	 * SourceWriter.
	 * 
	 * @param source
	 * @param methods
	 */
	private void insertAccessors(SourceWriter source, JMethod[] methods) {
		JParameter parameter;
		for (int i = 0; i < methods.length; i++) {
			JMethod methode = methods[i];
			JClassType type = methode.getEnclosingType();
			if ("java.lang.Object".equals(type.getQualifiedSourceName())) {
				continue;
			}
			if (methode.getName().startsWith("set")) { // setter
				source.println(methode.getReadableDeclaration() + "{");
				source.indent();
				parameter = methode.getParameters()[0];
				source.println("contenu." + methode.getName() + "("
						+ parameter.getName() + ");");
				source.outdent();
				source.println("}");
				source.println();
			} else if (methode.getName().startsWith("get")) { // getter
				source.println(methode.getReadableDeclaration() + "{");
				source.indent();
				source.println("return contenu." + methode.getName() + "();");
				source.outdent();
				source.println("}");
				source.println();
			}
		}
	}

	/**
	 * Write code common to all Wrapper in the SourceWriter. It writes:
	 * declaration of the object content and its getter and setter.
	 * 
	 * @param source
	 * @param methods
	 */
	private void insertWrapperBase(SourceWriter source, JMethod[] methods,
			JClassType classType) {
		String simpleName = classType.getSimpleSourceName();
		source.indent();
		source.println("private " + simpleName + " contenu;");
		source.println();
		source.println("public void setContent(BusinessObject contenu) {");
		source.indent();
		source.println("this.contenu = (" + simpleName + ") contenu;");
		source.outdent();
		source.println("}");
		source.println();
		source.println("public BusinessObject getContent() {");
		source.indent();
		source.println("return this.contenu;");
		source.outdent();
		source.println("}");
		source.println();
	}

	/**
	 * Write code common to all Wrapper in the SourceWriter. It writes the clone
	 * method.
	 * 
	 * @param source
	 * @param methods
	 */
	private void insertClone(SourceWriter source, JMethod[] methods,
			JClassType classType) {
		List bindableList = BindingParser.getBindableList();

		String simpleName = classType.getSimpleSourceName();
		source.println("public Object clone() {");
		source.indent();
		source.println(simpleName + " result = new " + simpleName + "();");

		for (int i = 0; i < methods.length; i++) {
			JMethod methode = methods[i];

			if (methode.getName().startsWith("set")
					&& methode.getParameters().length == 1
					&& !methode.isPrivate()) {
				JType paramType = methode.getParameters()[0].getType();
				JArrayType array = paramType.isArray();

				String getterName = null;
				if ((paramType.isPrimitive() != null)
						&& paramType.isPrimitive().getSimpleSourceName()
								.equals("boolean")) {
					// NB: Boolean has a classic getter starting with "get"
					getterName = "is" + methode.getName().substring(3) + "()";

				} else {
					getterName = "g" + methode.getName().substring(1) + "()";
				}

				if (array != null) {
					// the property is an array

					JPrimitiveType primitive = array.getComponentType()
							.isPrimitive();
					if (primitive != null) {
						// an array of primitives
						source.println("result." + methode.getName() + "(this."
								+ getterName + ");");

					} else {
						// an array of objects
						// FIXME deep copy
						source.println("FIXME result." + methode.getName()
								+ "(this." + getterName + ");");

						source.println("List objectList = (List)value;");
						source.println("Object[] objs = objectList.toArray();");
						source
								.print(array.getQualifiedSourceName()
										+ " out = ");
						source.print("new "
								+ array.getComponentType()
										.getQualifiedSourceName());
						source.println("[objs.length];");
						source
								.println("for(int i = 0; i < objs.length; i++) {");
						source.indent();
						source
								.println("out[i] = ("
										+ array.getComponentType()
												.getQualifiedSourceName()
										+ ")objs[i];");
						source.outdent();
						source.println("}");
						source.print("this." + methode.getName());
						source.println("(out);");
					}

				} else {
					JPrimitiveType primitive = paramType.isPrimitive();
					if (primitive == null) {
						// the property is an Object
						if (bindableList.contains(paramType
								.getQualifiedSourceName())) {
							// the property is a BusinessObject
							// it seems like GWT compiles every time a new java
							// file is generated, so we can't depend on any
							// other Wrapper by their name (deep copy)
							// source.println("result." + methode.getName() +
							// "(((" + paramType.getQualifiedSourceName()
							// + WRAPPER_SUFFIX + ") this." + getterName +
							// ").clone());");
							source.println("if (this." + getterName
									+ " != null) {");
							source.indent();
							source.println("result." + methode.getName() + "(("
									+ paramType.getQualifiedSourceName()
									+ ") BeanUtil.clone(this." + getterName
									+ "));");
							source.outdent();
							source.println("}");

						} else {
							// this is just a regular Object
							source.println("result." + methode.getName()
									+ "(this." + getterName + ");");
						}

					} else {
						source.println("result." + methode.getName() + "(this."
								+ getterName + ");");
					}
				}
			}
		}

		source.println("return result;");
		source.outdent();
		source.println("}");
		source.println();
	}

	/**
	 * Insert the getProperty method in the SourceWriter. This methods allows to
	 * get all the properties of the wrapped object with their name. It returns
	 * an Object object.
	 * 
	 * @param source
	 * @param methods
	 * @throws NotFoundException
	 */
	private void insertGetProperty(SourceWriter source, JMethod[] methods) {
		List bindableList = BindingParser.getBindableList();
		source
				.println("public Object getProperty(String property) throws DataException{");
		source.indent();

		for (int i = 0; i < methods.length; i++) {
			JClassType type = methods[i].getEnclosingType();
			if ("java.lang.Object".equals(type.getQualifiedSourceName())) {
				continue;
			}

			String methodName = methods[i].getName();
			JParameter[] methodParameters = methods[i].getParameters();
			JType returnType = methods[i].getReturnType();
			boolean isGetter = false;

			if (methodName.startsWith("get") && methodParameters.length == 0) {
				isGetter = true;
				source.println("if (property.equals(\""
						+ methodName.substring(3, 4).toLowerCase()
						+ methodName.substring(4) + "\")) {");
				source.indent();

			} else if (methodName.startsWith("is")
					&& methodParameters.length == 0) {
				isGetter = true;
				source.println("if (property.equals(\""
						+ methodName.substring(2, 3).toLowerCase()
						+ methodName.substring(3) + "\")) {");
				source.indent();
			}

			if (isGetter) {
				JArrayType array = returnType.isArray();
				if (array != null) {
					// we have an array, to convert into a List
					JPrimitiveType primitive = array.getComponentType()
							.isPrimitive();

					if (primitive != null) {
						// we must wrap the primitives in objects
						source
								.println(array.getQualifiedSourceName()
										+ " primitives = contenu." + methodName
										+ "();");
						source.println("ArrayList liste = new ArrayList();");
						source.println("if(primitives == null) {");
						source.indent();
						source.println("return liste;");
						source.outdent();
						source.println("} else {");
						source.indent();
						source
								.println("for(int i = 0; i < primitives.length; i++) {");
						source.indent();
						source.print("liste.add(");
						source.print(this.wrapPrimitive(primitive,
								"primitives[i]"));
						source.println(");");
						source.outdent();
						source.println("}");
						source.println("return liste;");
						source.outdent();
						source.println("}");

					} else {
						source.println(array.getQualifiedSourceName()
								+ " objects = contenu." + methodName + "();");
						source.println("ArrayList liste = new ArrayList();");
						source.println("if (objects == null) {");
						source.indent();
						source.println("return liste;");
						source.outdent();
						source.println("} else {");
						source.indent();
						source
								.println("for(int i = 0; i < objects.length; i++) {");
						source.indent();
						source.println("liste.add(objects[i]);");
						source.outdent();
						source.println("}");
						source.println("return liste;");
						source.outdent();
						source.println("}");
					}

				} else {
					// we have a simple property
					JPrimitiveType primitive = returnType.isPrimitive();
					if (primitive == null) {
						// this is an Object
						source.print(returnType.getQualifiedSourceName()
								+ " object");
						source.println(" = this." + methodName + "();");
						if (!bindableList.contains(returnType
								.getQualifiedSourceName())) {
							// the property is an Object but not a
							// BusinessObject
							source.println("if (object == null) {");
							source.indent();
							source.println("object = new "
									+ this.wrapObject(returnType) + ";");
							source.outdent();
							source.println("}");
						}
						source.println("return object;");

					} else {
						// wrap the primitive
						source.println("return "
								+ this.wrapPrimitive(primitive, "this."
										+ methodName + "()") + ";");
					}
				}
				source.outdent();
				source.print("} else ");
			}
		}

		source.println("{");
		source.indent();

		try {
			source
					.println("throw new DataException(\"The property \"+property+\" doesn\\'t exist in the "
							+ this.typeOracle.getType(this.typeName)
									.getSimpleSourceName()
							+ " object or is an array\");");
		} catch (NotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		source.outdent();
		source.println("}");
		source.outdent();
		source.println("}");
		source.println();
	}

	/**
	 * Insert the getProperty method in the SourceWriter. This methods allows to
	 * get all the properties of the wrapped object with their name. It returns
	 * an Object object.
	 * 
	 * @param source
	 * @param methods
	 * @throws NotFoundException
	 */
	private void insertIsNullProperty(SourceWriter source, JMethod[] methods) {
		List bindableList = BindingParser.getBindableList();
		source
				.println("public boolean isPropertyNull(String property) throws DataException{");
		source.indent();

		for (int i = 0; i < methods.length; i++) {
			JClassType type = methods[i].getEnclosingType();
			if ("java.lang.Object".equals(type.getQualifiedSourceName())) {
				continue;
			}

			String methodName = methods[i].getName();
			JParameter[] methodParameters = methods[i].getParameters();
			JType returnType = methods[i].getReturnType();
			boolean isGetter = false;

			if (methodName.startsWith("get") && methodParameters.length == 0) {
				isGetter = true;
				source.println("if (property.equals(\""
						+ methodName.substring(3, 4).toLowerCase()
						+ methodName.substring(4) + "\")) {");
				source.indent();

			} else if (methodName.startsWith("is")
					&& methodParameters.length == 0) {
				isGetter = true;
				source.println("if (property.equals(\""
						+ methodName.substring(2, 3).toLowerCase()
						+ methodName.substring(3) + "\")) {");
				source.indent();
			}

			if (isGetter) {
				JPrimitiveType primitive = returnType.isPrimitive();
				if (primitive == null) {
					// this is an Object
					source.print(returnType.getQualifiedSourceName()
							+ " object");
					source.println(" = this." + methodName + "();");
					// if
					// (!bindableList.contains(returnType.getQualifiedSourceName()))
					// {
					// // the property is an Object but not a
					// // BusinessObject
					// source.println("if (object == null) {");
					// source.indent();
					// source.println("object = new " +
					// this.wrapObject(returnType) + ";");
					// source.outdent();
					// source.println("}");
					// }
					source.println("return object == null;");

				} else {
					// wrap the primitive
					source.println("return false;");
					// source.println("return "
					// + this.wrapPrimitive(primitive, "this."
					// + methodName + "()") + ";");
				}
				// JArrayType array = returnType.isArray();
				// if (array != null) {
				// // we have an array, to convert into a List
				// JPrimitiveType primitive = array.getComponentType()
				// .isPrimitive();
				//
				// if (primitive != null) {
				// // we must wrap the primitives in objects
				// source
				// .println(array.getQualifiedSourceName()
				// + " primitives = contenu." + methodName
				// + "();");
				// source.println("ArrayList liste = new ArrayList();");
				// source.println("if(primitives == null) {");
				// source.indent();
				// source.println("return liste;");
				// source.outdent();
				// source.println("} else {");
				// source.indent();
				// source
				// .println("for(int i = 0; i < primitives.length; i++) {");
				// source.indent();
				// source.print("liste.add(");
				// source.print(this.wrapPrimitive(primitive,
				// "primitives[i]"));
				// source.println(");");
				// source.outdent();
				// source.println("}");
				// source.println("return liste;");
				// source.outdent();
				// source.println("}");
				//
				// } else {
				// source.println(array.getQualifiedSourceName()
				// + " objects = contenu." + methodName + "();");
				// source.println("ArrayList liste = new ArrayList();");
				// source.println("if (objects == null) {");
				// source.indent();
				// source.println("return liste;");
				// source.outdent();
				// source.println("} else {");
				// source.indent();
				// source
				// .println("for(int i = 0; i < objects.length; i++) {");
				// source.indent();
				// source.println("liste.add(objects[i]);");
				// source.outdent();
				// source.println("}");
				// source.println("return liste;");
				// source.outdent();
				// source.println("}");
				// }
				//
				// } else {
				// // we have a simple property
				// }
				source.outdent();
				source.print("} else ");
			}
		}

		source.println("{");
		source.indent();

		try {
			source
					.println("throw new DataException(\"The property \"+property+\" doesn\\'t exist in the "
							+ this.typeOracle.getType(this.typeName)
									.getSimpleSourceName()
							+ " object or is an array\");");
		} catch (NotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		source.outdent();
		source.println("}");
		source.outdent();
		source.println("}");
		source.println();
	}

	/**
	 * Insert the setProperty method in the SourceWriter. This methods allow to
	 * set all the properties of the wrapped object with their name from an
	 * Object object.
	 * 
	 * @param source
	 * @param methods
	 */
	private void insertSetProperty(SourceWriter source, JMethod[] methods) {
		source
				.println("public void setProperty(String property, Object value) throws DataException {");
		source.indent();

		for (int i = 0; i < methods.length; i++) {
			JMethod methode = methods[i];

			JClassType type = methode.getEnclosingType();
			if ("java.lang.Object".equals(type.getQualifiedSourceName())) {
				continue;
			}

			if (methode.getName().startsWith("set")
					&& methode.getParameters().length == 1
					&& !methode.isPrivate()) {
				source.println("if (property.equals(\""
						+ methode.getName().substring(3, 4).toLowerCase()
						+ methode.getName().substring(4) + "\")) { ");
				source.indent();

				JType paramType = methode.getParameters()[0].getType();
				JArrayType array = paramType.isArray();
				if (array != null) {
					// the property is an array

					JPrimitiveType primitive = array.getComponentType()
							.isPrimitive();
					if (primitive != null) {
						// an array of primitives
						source.println("List objectList = (List)value;");
						source.println("Object[] objs = objectList.toArray();");
						source.print(primitive.getSimpleSourceName());
						source.print("[] primitives = new ");
						source.print(primitive.getSimpleSourceName());
						source.println("[objs.length];");
						source
								.println("for (int i = 0; i < objs.length; i++) {");
						source.indent();
						source.print("primitives[i] = ");
						source.println(this.unwrapPrimitive(primitive,
								"objs[i]")
								+ ";");
						source.outdent();
						source.println("}");
						source.print("this." + methode.getName());
						source.println("(primitives);");

					} else {
						// an array of objects
						source.println("List objectList = (List)value;");
						source.println("Object[] objs = objectList.toArray();");
						source
								.print(array.getQualifiedSourceName()
										+ " out = ");
						source.print("new "
								+ array.getComponentType()
										.getQualifiedSourceName());
						source.println("[objs.length];");
						source
								.println("for(int i = 0; i < objs.length; i++) {");
						source.indent();
						source
								.println("out[i] = ("
										+ array.getComponentType()
												.getQualifiedSourceName()
										+ ")objs[i];");
						source.outdent();
						source.println("}");
						source.print("this." + methode.getName());
						source.println("(out);");
					}

				} else {
					JPrimitiveType primitive = paramType.isPrimitive();
					if (primitive == null) {
						source.println("this." + methode.getName() + "(("
								+ paramType.getQualifiedSourceName()
								+ ")value);");
					} else {
						source.println("this." + methode.getName() + "("
								+ this.unwrapPrimitive(primitive, "value")
								+ ");");
					}
				}

				source.outdent();
				source.print("} else ");
			}
		}

		source.println("{");
		try {
			source
					.println("throw new DataException(\"The property \"+property+\" doesn\\'t exist in the "
							+ this.typeOracle.getType(this.typeName)
									.getSimpleSourceName()
							+ " object or it is an array\");");
		} catch (NotFoundException e) {
			e.printStackTrace();
		}

		source.println("}");
		source.outdent();
		source.println("}");
	}

	private String wrapObject(JType type) {
		String name = type.getSimpleSourceName();

		if (name.equals("Long")) {
			return "Long(0L)";
		} else if (name.equals("Short")) {
			return "Short((short)0)";
		} else if (name.equals("Double")) {
			return "Double(0.00)";
		} else if (name.equals("Integer")) {
			return "Integer(0)";
		} else if (name.equals("Boolean")) {
			return "Boolean(false)";
		} else if (name.equals("Set")) {
			return "java.util.HashSet()";
		} else if (name.equals("List")) {
			return "java.util.ArrayList()";
		} else if (name.equals("Map")) {
			return "java.util.HashMap()";
		} else {
			return type.getQualifiedSourceName() + "()";
		}
	}

	private String wrapPrimitive(JPrimitiveType primType, String value) {
		String name = primType.getSimpleSourceName();
		if (name.equals("int")) {
			return "new Integer(" + value + ")";
		} else if (name.equals("float")) {
			return "new Float(" + value + ")";
		} else if (name.equals("double")) {
			return "new Double(" + value + ")";
		} else if (name.equals("long")) {
			return "new Long(" + value + ")";
		} else if (name.equals("boolean")) {
			return "new Boolean(" + value + ")";
		} else if (name.equals("byte")) {
			return "new Byte(" + value + ")";
		} else if (name.equals("short")) {
			return "new Short(" + value + ")";
		} else if (name.equals("char")) {
			return "new Character(" + value + ")";
		} else {
			return null;
		}
	}

	private String unwrapPrimitive(JPrimitiveType primType, String value) {
		String name = primType.getSimpleSourceName();
		if (name.equals("int")) {
			return "((Integer)" + value + ").intValue()";
		} else if (name.equals("float")) {
			return "((Float)" + value + ").floatValue()";
		} else if (name.equals("double")) {
			return "((Double)" + value + ").doubleValue()";
		} else if (name.equals("long")) {
			return "((Long)" + value + ").longValue()";
		} else if (name.equals("boolean")) {
			return "((Boolean)" + value + ").booleanValue()";
		} else if (name.equals("byte")) {
			return "((Byte)" + value + ").byteValue()";
		} else if (name.equals("short")) {
			return "((Short)" + value + ").shortValue()";
		} else if (name.equals("char")) {
			return "((Character)" + value + ").charValue()";
		} else {
			return null;
		}
	}

}
